What if the Netherlands used FPTP?

politics
mapview
ggpol

Examining the difference in election outcome between FPTP and D’Hondt (proportional representation).

Thomas Zwagerman https://twitter.com/thomzwa (Centre for Ecology & Hydrology)https://www.ceh.ac.uk/
03-24-2021
Show code
knitr::include_graphics("preview.jpg")

In this short article I am exploring what the Dutch political landscape would look like if it operated under a first-past-the-post (FPTP) system.

The election data for the Netherlands was downloaded from the Kiesraad. The shapefile was downloaded from ESRI NL.

Results Tweede Kamer Verkiezing 2021

These are the 2021 results summarised:

Show code
#sort in long format
df_results[is.na(df_results)] <- 0

df_pr <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(party, votes) %>% 
  group_by(party) %>% 
  summarise(votes=sum(votes))

df_pr$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pr$party)
df_pr$party <- gsub("Democraten.66..D66.","D66",df_pr$party)
df_pr$party <- gsub("X50PLUS","Partij 50PLUS",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("Forum.voor.Democratie","FvD",df_pr$party)
df_pr$party <- gsub("SP..Socialistische.Partij.","SP",df_pr$party)
df_pr$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pr$party)
df_pr$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pr$party)
df_pr$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pr$party)

dHondt <- function(votes, parties, n_seats = 150) {
  
  divisor.mat           <- sum(votes) / sapply(votes, "/", seq(1, n_seats, 1))
  colnames(divisor.mat) <- parties
  
  m.mat     <- tidyr::gather(as.data.frame(divisor.mat), key="name", value="value",
                             everything())
  m.mat     <- m.mat[rank(m.mat$value, ties.method = "random") <= n_seats, ]
  rle.seats <- rle(as.character(m.mat$name))
  
  if (sum(rle.seats$length) != n_seats)
    stop(paste("Number of seats distributed not equal to", n_seats))
  
  # fill up the vector with parties that got no seats
  if (any(!(parties %in% rle.seats$values))) {
    # add parties
    missing_parties <- parties[!(parties %in% rle.seats$values)]
    for (party in missing_parties) {
      rle.seats$lengths <- c(rle.seats$lengths, 0)
      rle.seats$values  <- c(rle.seats$values, party)
    }
    # sort results
    rle.seats$lengths <- rle.seats$lengths[match(parties, rle.seats$values)]
    rle.seats$values  <- rle.seats$values[match(parties, rle.seats$values)]
  }
  
  rle.seats$length
  
}

df_pr$seats<- dHondt(df_pr$votes,df_pr$party,150)
df_pr <- df_pr %>% 
  filter(seats>0) %>% 
  as.data.frame()
df_pr <- df_pr[order(df_pr$votes,decreasing =T),] 
df_pr <- left_join(df_pr,colourscheme)

df_pr$total <- sum(df_pr$votes)
df_pr$percentage <- (df_pr$votes/df_pr$total)*100
df_pr$percentage <- round(df_pr$percentage,1)

resultaat <- df_pr %>% 
  select(party,votes,seats,percentage)
kable(resultaat)
party votes seats percentage
VVD 2279126 34 22.3
D66 1565862 24 15.3
PVV 1124482 17 11.0
CDA 990601 15 9.7
SP 623371 9 6.1
PvdA 597192 9 5.8
GROENLINKS 537308 8 5.3
FvD 523083 8 5.1
PvdD 399751 6 3.9
ChristenUnie 351275 5 3.4
Volt 252480 3 2.5
JA21 246620 3 2.4
SGP 215249 3 2.1
DENK 211238 3 2.1
Partij 50PLUS 106702 1 1.0
BBB 104319 1 1.0
BIJ1 87238 1 0.9

Visualising parliament using ggpol

Let’s start with examining the composition of the Tweede Kamer - under a proportional representation (D’Hondt) system. This is what the Tweede Kamer looks like currently:

Show code
#plot proportional table with ggpol's geom_parliament.
ggplot(df_pr) +
  ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") + 
  #highlight the party in control of the House with a black line
  scale_fill_manual(values = df_pr$colour, 
                    labels = df_pr$party)+
  coord_fixed()+
  theme_void()

Now let’s have a look at what it would look like under First-past-the-Post (FPTP), with one MP elected per Gemeente.

Show code
#sort in long format
df_long <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(regio =RegioNaam, code = RegioCode,party, votes) %>% 
  filter(!is.na(votes)) %>% 
  group_by(code) %>% 
  mutate(total = sum(votes)) %>% 
  slice_max(votes)

#convert to percentage, round and remove the G from the codes to match up
df_long$percentage <- (df_long$votes/df_long$total)*100
df_long$percentage <- round(df_long$percentage,1)
df_long$code <- gsub("G","",df_long$code)

#fix the names to acronyms for consistency
df_long$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_long$party)
df_long$party <- gsub("Democraten.66..D66.","D66",df_long$party)
df_long$party <- gsub("X50PLUS","Partij 50PLUS",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("Forum.voor.Democratie","FvD",df_long$party)
df_long$party <- gsub("SP..Socialistische.Partij.","SP",df_long$party)
df_long$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_long$party)
df_long$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_long$party)
df_long$party <- gsub("Partij.voor.de.Dieren","PvdD",df_long$party)
df_long$party <- as.factor(df_long$party)
#tidy up region names to exclude -'s and 's
df_long$regio <- gsub("'","",df_long$regio)
df_long$regio <- gsub("-"," ",df_long$regio)
#join names with corresponding colour scheme
df_long <- left_join(df_long,colourscheme)

#Tabulate the results in order to get a seat tally by party
fptp_results <- table(df_long$party) %>% as.data.frame()
colnames(fptp_results) <- c("party","seats")
fptp_results <- left_join(fptp_results,colourscheme)

#use geom_parliament from ggpol to visualise
ggplot(fptp_results) +
  ggpol::geom_parliament(aes(seats = seats, fill = party),color="black") + 
  #highlight the party in control of the House with a black line
  scale_fill_manual(values = fptp_results$colour, 
                    labels = fptp_results$party)+
  coord_fixed()+
  theme_void()

Now, there are a couple of obvious differences:

But also some obvious caveats:

Election results on a map

We can also show these results on a map - this in an interactive map which allows you to explore the individual election results for each Gemeente.

Show code
#Prepare data for results per gemeente
#sort in long format
df_pie <- df_results %>% 
  pivot_longer(cols = VVD:Modern.Nederland, names_to = "party", values_to = "votes") %>%
  select(regio =RegioNaam, code = RegioCode,party, votes) %>% 
  filter(!is.na(votes)) %>% 
  group_by(code) %>% 
  mutate(total = sum(votes)) 
df_pie$code <- gsub("G","",df_pie$code)

#bit of code to automatically remove the gemeentes that can be found in the results, but not on the map.
#these are the gemeentes Bonaire, Saba and Sint Eustasius which are in the Caribbean.
names_df <- unique(df_pie$code)
names_shp <- unique(shp_gemeentes$Gemeenteco)
names_remove <- names_df[!names_df %in% names_shp]
#remove Bonaire, Saba, Sint Eustasius
df_pie <- df_pie %>% 
  filter(!code %in% names_remove)
#round percentages 
df_pie$percentage <- (df_pie$votes/df_pie$total)*100
df_pie$percentage <- round(df_pie$percentage,1)

#change name acronyms
#this step is being repeated, initially seperate script - move up to first chunk?
df_pie$party <- gsub("Partij.van.de.Arbeid..P.v.d.A..","PvdA",df_pie$party)
df_pie$party <- gsub("Democraten.66..D66.","D66",df_pie$party)
df_pie$party <- gsub("X50PLUS","Partij 50PLUS",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("Forum.voor.Democratie","FvD",df_pie$party)
df_pie$party <- gsub("SP..Socialistische.Partij.","SP",df_pie$party)
df_pie$party <- gsub("PVV..Partij.voor.de.Vrijheid.","PVV",df_pie$party)
df_pie$party <- gsub("Staatkundig.Gereformeerde.Partij..SGP.","SGP",df_pie$party)
df_pie$party <- gsub("Partij.voor.de.Dieren","PvdD",df_pie$party)
df_pie$party <- as.factor(df_pie$party)

df_pie$party <- as.character(df_pie$party)
df_pie$percentage <- as.numeric(df_pie$percentage)

#make a list of pie chart plots, in the order of code - this way it will correspond with the rows in the shapefile.
pie_plot_list <- lapply(unique(df_pie$code), function(i) {
  #filter and group info per gemeente code
  regio_df <- df_pie %>% 
    filter(code == i) %>%
    dplyr::ungroup() %>% 
    select(party,percentage,regio) %>%
    filter(percentage >0.1) %>% 
    as.data.frame()
  #order by the vote tally 
  regio_df <- regio_df[order(regio_df$percentage,decreasing = T),]
  #rejoin with the colourscheme
  regio_df <- left_join(regio_df,colourscheme)
  
  #plot as a bar chart, important to paste the corresponding gemeente name
  ggplot(regio_df) +
    geom_bar(aes(x = "", y = percentage, fill = party),
             stat = "identity", colour = "black", width =1) +
    #geom_text(aes(x = "", y = pct, label = percent(pct)), position = position_stack(vjust = 0.5))+
    coord_polar("y", start = 0) +
    labs(x = NULL, y = NULL, fill = NULL, 
         title = paste("Verkiezingsuitslag",unique(regio_df$regio))) +
    theme_classic()+
    scale_fill_manual(values = regio_df$colour, 
                      limits = regio_df$party)+ 
    theme(axis.line = element_blank(),
          axis.text = element_blank(),
          axis.ticks = element_blank(),
          plot.title = element_text(hjust = 0.5, color = "black"),
          legend.position = "bottom")
})

#remove saba, st eustasius etc.
df_long <- df_long %>% 
  filter(!code %in% names_remove)

#link to spatial
shp_fptp <- left_join(shp_gemeentes, df_long, by = c("Gemeenteco"="code"))
shp_fptp <- shp_fptp %>% 
  filter(percentage >0.1)
#I need to make the shapefile alphabetical to match with the pie chart list
#This means removing apostrophe's at 's-Gravenhage and 's-Hertogenbosch
shp_fptp$Gemeentena <- gsub("'","",shp_gemeentes$Gemeentena)
#reorder
shp_fptp <- shp_fptp[order(shp_fptp$Gemeentena),]
#reset rownumbers so they line up
rownames(shp_fptp) <- 1:nrow(shp_fptp)
#use mapview to display colour by party, add popupgrah which refers to the list of pie charts made earlier.
mapview(shp_fptp,
        zcol = "party",
        col.regions = shp_fptp$colour,
        popup = popupGraph(pie_plot_list, width = 450,height =300))

Clicking on each gemeente will display a pie chart with its individual results. Some geographical patterns to note is that the Christian parties (SGP, CU) tend to do well in the so-called “bible-belt”. More Liberal and Green parties (D66, GroenLinks) perform well in the big cities. In rural areas where agriculture is an important industry, the CDA is a popular choice. The populist right-wing party, the PVV, does well on the fringes of the Netherlands - far from the centre of power where trust in politics is low.